(*
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *)
open! IStd

type violation = {lhs: AnnotatedNullability.t; rhs: InferredNullability.t} [@@deriving compare]

module ReportableViolation = struct
  type t = {nullsafe_mode: NullsafeMode.t; violation: violation}

  type assignment_type =
    | PassingParamToFunction of function_info
    | AssigningToField of Fieldname.t
    | ReturningFromFunction of Procname.Java.t
  [@@deriving compare]

  and function_info =
    { param_signature: AnnotatedSignature.param_signature
    ; kind: AnnotatedSignature.kind
    ; actual_param_expression: string
    ; param_position: int
    ; function_procname: Procname.Java.t }

  let from nullsafe_mode ({lhs; rhs} as violation) =
    let falls_under_optimistic_third_party =
      Config.nullsafe_optimistic_third_party_params_in_non_strict
      && NullsafeMode.equal nullsafe_mode Default
      && Nullability.equal (AnnotatedNullability.get_nullability lhs) ThirdPartyNonnull
    in
    let is_non_reportable =
      falls_under_optimistic_third_party
      || (* In certain modes, we trust rhs to be non-nullable and don't report violation *)
      Nullability.is_considered_nonnull ~nullsafe_mode (InferredNullability.get_nullability rhs)
    in
    if is_non_reportable then None else Some {nullsafe_mode; violation}


  let get_severity {nullsafe_mode} = NullsafeMode.severity nullsafe_mode

  let get_origin_opt assignment_type origin =
    let should_show_origin =
      match assignment_type with
      | PassingParamToFunction {actual_param_expression} ->
          not
            (ErrorRenderingUtils.is_object_nullability_self_explanatory
               ~object_expression:actual_param_expression origin)
      | AssigningToField _ | ReturningFromFunction _ ->
          true
    in
    if should_show_origin then Some origin else None


  let pp_param_name fmt mangled =
    let name = Mangled.to_string mangled in
    if String.is_substring name ~substring:"_arg_" then
      (* The real name was not fetched for whatever reason, this is an autogenerated name *)
      Format.fprintf fmt ""
    else Format.fprintf fmt "(%a)" MarkupFormatter.pp_monospaced name


  let mk_description_for_bad_param_passed
      {kind; param_signature; actual_param_expression; param_position; function_procname}
      ~param_nullability_kind ~nullability_evidence =
    let nullability_evidence_as_suffix =
      Option.value_map nullability_evidence ~f:(fun evidence -> ": " ^ evidence) ~default:""
    in
    let annotated_param_nullability = param_signature.param_annotated_type.nullability in
    let module MF = MarkupFormatter in
    let argument_description =
      if String.equal actual_param_expression "null" then "is `null`"
      else
        let nullability_descr =
          match param_nullability_kind with
          | ErrorRenderingUtils.UserFriendlyNullable.Null ->
              "`null`"
          | ErrorRenderingUtils.UserFriendlyNullable.Nullable ->
              "nullable"
        in
        Format.asprintf "%a is %s" MF.pp_monospaced actual_param_expression nullability_descr
    in
    match AnnotatedNullability.get_nullability annotated_param_nullability with
    | Nullability.Null ->
        Logging.die Logging.InternalError "Unexpected param nullability: Null"
    | Nullability.Nullable ->
        Logging.die Logging.InternalError "Passing anything to a nullable param should be allowed"
    | Nullability.ThirdPartyNonnull ->
        (* This is a special case. While for FB codebase we can assume "not annotated hence not nullable" rule for all_whitelisted signatures,
           This is not the case for third party functions, which can have different conventions,
           So we can not just say "param is declared as non-nullable" like we say for FB-internal or modelled case:
           param can be nullable according to API but it was just not annotated.
           So we phrase it differently to remain truthful, but as specific as possible.
        *)
        let suggested_third_party_sig_file =
          ThirdPartyAnnotationInfo.lookup_related_sig_file_for_proc
            (ThirdPartyAnnotationGlobalRepo.get_repo ())
            function_procname
        in
        let where_to_add_signature =
          Option.value_map suggested_third_party_sig_file
            ~f:(fun sig_file_name ->
              ThirdPartyAnnotationGlobalRepo.get_user_friendly_third_party_sig_file_name
                ~filename:sig_file_name )
              (* this can happen when third party is registered in a deprecated way (not in third party repository) *)
            ~default:"the third party signature storage"
        in
        let procname_str = Procname.Java.to_simplified_string ~withclass:true function_procname in
        Format.asprintf
          "Third-party %a is missing a signature that would allow passing a nullable to param \
           #%d%a. Actual argument %s%s. Consider adding the correct signature of %a to %s."
          MF.pp_monospaced procname_str param_position pp_param_name param_signature.mangled
          argument_description nullability_evidence_as_suffix MF.pp_monospaced procname_str
          where_to_add_signature
    | Nullability.LocallyCheckedNonnull
    | Nullability.LocallyTrustedNonnull
    | Nullability.UncheckedNonnull
    | Nullability.StrictNonnull ->
        let nonnull_evidence =
          match kind with
          | FirstParty | ThirdParty Unregistered ->
              ""
          | ThirdParty ModelledInternally ->
              " (according to nullsafe internal models)"
          | ThirdParty (InThirdPartyRepo {filename; line_number}) ->
              Format.sprintf " (see %s at line %d)"
                (ThirdPartyAnnotationGlobalRepo.get_user_friendly_third_party_sig_file_name
                   ~filename)
                line_number
        in
        Format.asprintf "%a: parameter #%d%a is declared non-nullable%s but the argument %s%s."
          MF.pp_monospaced
          (Procname.Java.to_simplified_string ~withclass:true function_procname)
          param_position pp_param_name param_signature.mangled nonnull_evidence argument_description
          nullability_evidence_as_suffix


  let get_issue_type = function
    | PassingParamToFunction _ ->
        IssueType.eradicate_parameter_not_nullable
    | AssigningToField _ ->
        IssueType.eradicate_field_not_nullable
    | ReturningFromFunction _ ->
        IssueType.eradicate_return_not_nullable


  let mk_nullsafe_issue_for_explicitly_nullable_values ~assignment_type ~rhs_origin
      ~explicit_rhs_nullable_kind ~assignment_location =
    let nullability_evidence =
      get_origin_opt assignment_type rhs_origin
      |> Option.bind ~f:(fun origin -> TypeOrigin.get_description origin)
    in
    let nullability_evidence_as_suffix =
      Option.value_map nullability_evidence ~f:(fun evidence -> ": " ^ evidence) ~default:""
    in
    let module MF = MarkupFormatter in
    let alternative_method_description =
      ErrorRenderingUtils.find_alternative_nonnull_method_description rhs_origin
    in
    let alternative_recommendation =
      Option.value_map alternative_method_description
        ~f:(fun descr ->
          Format.asprintf " If you don't expect null, use %a instead." MF.pp_monospaced descr )
        ~default:""
    in
    let error_message =
      match assignment_type with
      | PassingParamToFunction function_info ->
          Format.sprintf "%s%s"
            (mk_description_for_bad_param_passed function_info ~nullability_evidence
               ~param_nullability_kind:explicit_rhs_nullable_kind)
            alternative_recommendation
      | AssigningToField field_name ->
          let rhs_description =
            match explicit_rhs_nullable_kind with
            | ErrorRenderingUtils.UserFriendlyNullable.Null ->
                "`null`"
            | ErrorRenderingUtils.UserFriendlyNullable.Nullable ->
                "a nullable"
          in
          Format.asprintf "%a is declared non-nullable but is assigned %s%s.%s" MF.pp_monospaced
            (Fieldname.get_field_name field_name)
            rhs_description nullability_evidence_as_suffix alternative_recommendation
      | ReturningFromFunction function_proc_name ->
          let return_description =
            match explicit_rhs_nullable_kind with
            | ErrorRenderingUtils.UserFriendlyNullable.Null ->
                (* Return `null` in all_whitelisted branches *)
                "`null`"
            | ErrorRenderingUtils.UserFriendlyNullable.Nullable ->
                "a nullable value"
          in
          Format.asprintf "%a: return type is declared non-nullable but the method returns %s%s.%s"
            MF.pp_monospaced
            (Procname.Java.to_simplified_string ~withclass:false function_proc_name)
            return_description nullability_evidence_as_suffix alternative_recommendation
    in
    let issue_type = get_issue_type assignment_type in
    (error_message, issue_type, assignment_location)


  let get_description ~assignment_location assignment_type {nullsafe_mode; violation= {rhs}} =
    let rhs_origin = InferredNullability.get_origin rhs in
    let user_friendly_nullable =
      ErrorRenderingUtils.UserFriendlyNullable.from_nullability
        (InferredNullability.get_nullability rhs)
      |> IOption.if_none_eval ~f:(fun () ->
             Logging.die InternalError
               "get_description:: Assignment violation should not be possible for non-nullable \
                values on right hand side" )
    in
    match user_friendly_nullable with
    | ErrorRenderingUtils.UserFriendlyNullable.UntrustedNonnull untrusted_kind ->
        (* Attempt to assigning a value which is not explictly declared as nullable,
           but still can not be trusted in this particular mode.
        *)
        ErrorRenderingUtils.mk_nullsafe_issue_for_untrusted_values ~nullsafe_mode ~untrusted_kind
          ~bad_usage_location:assignment_location rhs_origin
    | ErrorRenderingUtils.UserFriendlyNullable.ExplainablyNullable explicit_kind ->
        (* Attempt to assigning a value that can be explained to the user as nullable. *)
        mk_nullsafe_issue_for_explicitly_nullable_values ~assignment_type ~rhs_origin
          ~explicit_rhs_nullable_kind:explicit_kind ~assignment_location
end

let check ~lhs ~rhs =
  let is_subtype =
    Nullability.is_subtype
      ~supertype:(AnnotatedNullability.get_nullability lhs)
      ~subtype:(InferredNullability.get_nullability rhs)
  in
  Result.ok_if_true is_subtype ~error:{lhs; rhs}
